'''
    浏览器selenium封装库

    完全封装selenium的库内容。
    包含一些列处理浏览器操作的问题

'''
import os
import sys
import time
import traceback

from PyQt5 import QtWidgets as QW
from PyQt5 import QtCore as QC
from pyx_package.script import BaseBehavior
from pyx_package.ps_paramete import PsParameter
from selenium import webdriver as wd
from selenium.webdriver.support.select import Select
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By

PYX_MODULE_NAME = "browser-selenium"

def GetExceptStr():
    '''
        获取异常字符串
    '''

    msg = ""
    for line in traceback.TracebackException(
        type(sys.exc_info()[1]), sys.exc_info()[1], sys.exc_info()[2]).format():
        msg += line + "\n"

    return msg

'''
    指令数据
    一个指令所包含的所有数据
    也就是一个InstructData对象就是一条可执行的操作
'''
class InstructData(object):

    class ValueInputMode(int):

        List = 1
        Dict = 2
        Const = 3

    def __init__(self) -> None:

        self.serial = -1    #执行序号
        
        #选择器信息
        self.id = ""
        self.name = ""
        self.tag_name = ""
        self.class_name = ""
        self.type = ""
        self.src = ""
        self.href = ""
        self.xpath = ""

        #指令信息
        self.instruct = None

        #指令参数的输入格式
        self.inst_input_mode = None
        self.inst_input_value = None

'''
    指令集处理
'''

class Instruct(object):

    InputText = '输入文本'
    Clear = '清空文本'
    Enter = '回车确认'
    Click = '点击'
    RightClick = '右键点击'
    DoubleClick = "双击"
    MoveToEle = "悬停"
    RunJS = '执行js'
    NewLink = '跳转到新的页面'
    Sleep = '休眠(自由操作浏览器时间)'
    ComboBoxByText = "下拉框选择(通过显示文本)"
    ComboBoxByIndex = "下拉框选择(通过索引,从0开始)"
    ComboBoxByValue = "下拉框选择(通过value属性)"
    FileInput = "文件输入"
    SwitchTo_Element = "切换标签(通过含有超链接的元素)"
    
    inst_list = [
        InputText, Clear, Enter, Click,
        RightClick, DoubleClick, MoveToEle, 
        FileInput, RunJS, NewLink, Sleep, SwitchTo_Element,
        ComboBoxByText, ComboBoxByIndex, ComboBoxByValue
    ]

    def __init__(self, driver) -> None:
        self.driver = driver

    @staticmethod
    def FindElements(driver: wd.Chrome, inst:InstructData):
        '''根据给定指令查找指定元素'''

        elements = []

        #如果有xpath就不找别的了
        if inst.xpath != "":
            eles = driver.find_elements(By.XPATH, inst.xpath)
            elements.extend(eles)
            return elements

        #先查找id(有id就不查找别的了)
        if inst.id != "":
            ele = driver.find_element(By.ID, inst.id)
            elements.append(ele)
            return elements
        
        else:
            
            #这个是一个临时容器，用于筛选
            temp_elements = []

            if inst.class_name != "":
                eles = driver.find_elements(By.CLASS_NAME, inst.class_name)
                temp_elements.extend(eles)

            if inst.name != "":

                eles = driver.find_elements(By.NAME, inst.name)
                
                #如果temp_ele不为空，这里需要筛选，而不是添加
                if len(temp_elements) > 0:
                    temp_elements = list(set(temp_elements) & set(eles))

                else:
                    temp_elements.extend(eles)

            if inst.tag_name != "":

                eles = driver.find_elements(By.TAG_NAME, inst.tag_name)
                
                #如果temp_ele不为空，这里需要筛选，而不是添加
                if len(temp_elements) > 0:
                    temp_elements = list(set(temp_elements) & set(eles))

                else:
                    temp_elements.extend(eles)

            if inst.type != "":

                #如果src不为空，则遍历所有的templist，将属性不等于src的全部移除
                remove_list = []

                for temp_ele in temp_elements:
                    if temp_ele.get_attribute("type") != inst.type:
                        remove_list.append(temp_ele)

                #移除
                for rv in remove_list:
                    temp_elements.remove(rv)

            if inst.src != "":

                #如果src不为空，则遍历所有的templist，将属性不等于src的全部移除
                remove_list = []

                for temp_ele in temp_elements:
                    if temp_ele.get_attribute("src") != inst.src:
                        remove_list.append(temp_ele)

                #移除
                for rv in remove_list:
                    temp_elements.remove(rv)

            if inst.href != "":

                #如果src不为空，则遍历所有的templist，将属性不等于src的全部移除
                remove_list = []

                for temp_ele in temp_elements:
                    
                    if temp_ele.get_attribute("href") != inst.href:
                        remove_list.append(temp_ele)

                #移除
                for rv in remove_list:
                    temp_elements.remove(rv)

            elements.extend(temp_elements)

        return elements   
    
    @staticmethod
    def Exec(driver, element:WebElement, inst, value = ""):
        '''
            执行一行命令
            element:需要执行命令的html元素对象
            inst:需要执行的指令
            value:执行指令需要传入的值（可能用不到）
        '''
        
        if inst.instruct == Instruct.InputText:

            element.send_keys(str(value))

        if inst.instruct == Instruct.FileInput:
            element.send_keys(str(value))

        elif inst.instruct == Instruct.Clear:
            element.clear()
        
        elif inst.instruct == Instruct.Enter:
            element.submit()
        
        elif inst.instruct == Instruct.Click:
            element.click()
        
        elif inst.instruct == Instruct.RightClick:
            ActionChains(driver).context_click(element).perform()
        
        elif inst.instruct == Instruct.DoubleClick:
            ActionChains(driver).double_click(element).perform()
        
        elif inst.instruct == Instruct.MoveToEle:
            ActionChains(driver).move_to_element(element).perform()
        
        elif inst.instruct == Instruct.RunJS:
            driver.execute_script(value)
        
        elif inst.instruct == Instruct.Sleep:
            try: 
                time.sleep(float(value))
            except:
                pass
            
        elif inst.instruct == Instruct.NewLink:
            driver.get(value)

        elif inst.instruct == Instruct.ComboBoxByText:
            Select(element).select_by_visible_text(str(value))     
        elif inst.instruct == Instruct.ComboBoxByIndex:
            Select(element).select_by_index(int(value)) 
        elif inst.instruct == Instruct.ComboBoxByValue:
            Select(element).select_by_value(str(value))  

        elif inst.instruct == Instruct.SwitchTo_Element:
            driver.switch_to.frame(element)

    @staticmethod
    def IsNeedElementInst(instruct_mode):
        '''
            判断一个指令是否依赖于一个元素标签
        '''

        if  instruct_mode == Instruct.Sleep or \
            instruct_mode == Instruct.NewLink or \
            instruct_mode == Instruct.RunJS:
            return False
        else:
            return True

    @staticmethod
    def IsHaveValueInstr(instruct_mode):
        '''
            判断一个指令是否是带参数(value)的
        '''
        if  instruct_mode == Instruct.InputText or \
            instruct_mode == Instruct.FileInput or \
            instruct_mode == Instruct.RunJS or \
            instruct_mode == Instruct.Sleep or \
            instruct_mode == Instruct.NewLink or \
            instruct_mode == Instruct.ComboBoxByIndex or \
            instruct_mode == Instruct.ComboBoxByText or \
            instruct_mode == Instruct.ComboBoxByValue:
            return True
        else:
            return False

    @staticmethod
    def LoadFromFile(file_path):
        '''
            加载一系列命令通过给定文件
        '''

        #指令集
        inst_list = []

        #cookie集
        cookies = []

        tree = ET.parse(file_path)
        if not tree: return None

        root = tree.getroot()

        #获取方案名称
        url = root.attrib["url"]

        for child in root:

            if child.tag == 'InstData':

                for inst_node in child:

                    inst_data = InstructData()

                    inst_data.serial = int(inst_node.attrib["Serial"])
                    inst_data.id = str(inst_node.attrib["ID"])
                    inst_data.name = str(inst_node.attrib["Name"])
                    inst_data.tag_name = str(inst_node.attrib["TagName"])
                    inst_data.class_name = str(inst_node.attrib["Class"])
                    inst_data.type = str(inst_node.attrib["Type"])
                    inst_data.src = str(inst_node.attrib["Src"])
                    inst_data.href = str(inst_node.attrib["Href"])
                    inst_data.xpath = str(inst_node.attrib["XPath"])
                    inst_data.instruct = str(inst_node.attrib["Instruct"])

                    if Instruct.IsHaveValueInstr(inst_data.instruct):
                        inst_data.inst_input_mode = int(inst_node.attrib["InstrcutMode"])

                        if inst_data.inst_input_mode == InstructData.ValueInputMode.List:
                            inst_data.inst_input_value = int(inst_node.attrib["InstrcutValue"])
                        elif inst_data.inst_input_mode == InstructData.ValueInputMode.Dict:
                            inst_data.inst_input_value = inst_node.attrib["InstrcutValue"]
                        elif inst_data.inst_input_mode == InstructData.ValueInputMode.Const:
                            inst_data.inst_input_value = inst_node.attrib["InstrcutValue"]

                    inst_list.append(inst_data)

            elif  child.tag == 'CookieData':

                for inst_node in child:

                    cookie = {}

                    cookie["name"] = inst_node.attrib["name"]
                    cookie["domain"] = inst_node.attrib["domain"]
                    cookie["value"] = inst_node.attrib["value"]
                    cookie["path"] = inst_node.attrib["path"]
                    cookie["expirationDate"] = inst_node.attrib["expirationDate"]

                    cookies.append(cookie)
        return inst_list, cookies
    
    @staticmethod
    def ExportInstList(inst_list, file_path, cookies, url = ""):
        '''
            将指令导出为已给文件列表
        '''
        
        root = ET.Element('SeleniumData', {"url": url})
        tree = ET.ElementTree(root)

        inst_node = ET.Element('InstData')
        #写入数据节点
        for inst_data in inst_list:
            inst_data:InstructData
            mode = str(inst_data.inst_input_mode) if inst_data.inst_input_mode != None else ""
            value = str(inst_data.inst_input_value) if inst_data.inst_input_value != None else ""

            node = Element("inst_node", {
                "Serial": str(inst_data.serial),
                'ID': inst_data.id,
                'Name': inst_data.name,
                'TagName': inst_data.tag_name,
                'Class': inst_data.class_name,
                'Type': inst_data.type,
                'Src': inst_data.src,
                'Href': inst_data.href,
                'XPath': inst_data.xpath,
                'Instruct': inst_data.instruct,
                'InstrcutMode': mode,
                'InstrcutValue': value
                })

            inst_node.append(node)
        
        root.append(inst_node)
        
        #导出cookie
        cookie_ele = ET.Element('CookieData')
        for cookie in cookies:

            node = Element("cookie_node", {
                "name": cookie['name'],
                "domain": cookie['domain'],
                "value": cookie['value'],
                "path": cookie['path'],
                "expirationDate": cookie['expirationDate'],
                })

            cookie_ele.append(node)
        
        root.append(cookie_ele)

        #格式化xml格式
        indent(root)
        tree.write(file_path, xml_declaration=True)

'''
    处理xml的配置
'''
import xml.etree.ElementTree as ET
from xml.etree.ElementTree import Element

''' xml 格式格式化'''
def indent(elem, level=0):
    i = "\n" + level*"\t"
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "\t"
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for elem in elem:
            indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i

''' Script Behavior'''
class ScriptBehavior(BaseBehavior):

    def __init__(self) -> None:
        super().__init__()
        
        self.SetExeMode("function")

        self.InitParams()
        self.InitAttr()

    def InitParams(self):
        '''
            初始化参数信息
        '''

        #网址
        self.p_url = self.CreateParam("url", "网址", PsParameter.Input, str, "")

        #浏览器文件位置
        self.p_inst_path = self.CreateParam("instruct path", "指令集文件路径", PsParameter.Input, str, '')

        #驱动信息
        self.p_driver = self.CreateParam("driver", "浏览器驱动", PsParameter.Input, str, 'Chrome')
        self.p_driver.value_input_mode = PsParameter.ComboBoxMode
        self.p_driver.SetOptionitems(['Chrome', 'Edge', 'Firefox', "Ie", 'Safari'])
        self.p_driver.must = False

        #浏览器文件位置
        self.p_interval = self.CreateParam("interval", "指令执行间隔", PsParameter.Input, float, 0.1)
        self.p_interval.must = False

        #输入指令值
        self.p_instruct_value = self.CreateParam("instruct value", "指令输入值(列表/字典 类型)", PsParameter.Input, any)

    def InitAttr(self):
        '''
            初始化属性
        '''
        
        self._driver = None

    def InitDriver(self):
        '''
            创建selenium的驱动
        '''

        try:
            if self.p_driver.value == "Chrome":
                self._driver = wd.Chrome()

            elif self.p_driver.value == "Edge":
                self._driver = wd.Edge()

            elif self.p_driver.value == "Firefox":
                self._driver = wd.Firefox()

            elif self.p_driver.value == "Ie":
                self._driver = wd.Ie()

            elif self.p_driver.value == "Safari":
                self._driver = wd.Safari()

        except:
            self.Error("驱动创建失败：" + GetExceptStr())
            self._driver = None

    def DoStep(self):
        # 判断指令文件是否存在
        if self.p_inst_path.value and not os.path.exists(self.p_inst_path.value):
            self.Debug("指令集文件不存在！", "error")
            return 
        
        #读取指令和cookie
        inst_list, cookies = Instruct.LoadFromFile(self.p_inst_path.value)

        self.InitDriver()

        self._driver.get(self.p_url.value)

        for cookie in cookies:

            try:
                self._driver.add_cookie(cookie)
            except:
                pass
        
        #加载cookie后刷新网页
        self._driver.refresh()

        #检测驱动是否启动
        if not self._driver:
            self.Debug("浏览器驱动启动失败，请检查电脑是否安装最新版浏览器或驱动选择是否正确！", "error")
            return 

        self.DealInstruct(inst_list)
        
    def DealInstruct(self, inst_list):
        '''
            处理命令
        '''

        if not inst_list:
            self.Debug("指令文件读取失败！", "error")
            return
        
        for instruct in inst_list:

            # 判断命令是否依赖于元素
            if Instruct.IsNeedElementInst(instruct.instruct):

                #先查找当前指令的元素
                elements = Instruct.FindElements(self._driver, instruct)
                for element in elements:

                    inst_value = None
        
                    if Instruct.IsHaveValueInstr(instruct.instruct):
                        inst_value = self.GetInstructValue(instruct)

                    if inst_value == None:
                        self.Error("指令 {} 执行失败，未能获取指令的值内容！".format(instruct.instruct))

                    Instruct.Exec(self._driver, element, instruct, inst_value)

            else:
                inst_value = self.GetInstructValue(instruct)

                #如果一个指令需要值但值为空
                if Instruct.IsHaveValueInstr(instruct.instruct) and inst_value == None:
                    self.Error("指令 {} 执行失败，未能获取指令的值内容！".format(instruct.instruct))

                Instruct.Exec(self._driver, None, instruct, inst_value)

            #暂停间隔时间
            time.sleep(self.p_interval.value)

        self._driver.delete_all_cookies()

    def GetInstructValue(self, inst:InstructData):
        '''
            获取一个指令的输入值
        '''

        inst_value = self.p_instruct_value.value

        
        #判断当前输入模式
        if inst.inst_input_mode == InstructData.ValueInputMode.List:

            #判断当前输入类型是不是列表
            if type(inst_value) != list:
                self.Debug("指令的输入模式与当前指令参数输入类型不同！ (需要一个list)", "warning")
                return None
            
            #判断数组有没有越界
            if inst.inst_input_value > len(inst_value) + 1:
                self.Debug("指令值读取错误, 列表索引越界", "warning")
                return None
            
            return inst_value[inst.inst_input_value]
        
        elif inst.inst_input_mode == InstructData.ValueInputMode.Dict:

            #判断当前输入类型是不是列表
            if type(inst_value) != dict:
                self.Debug("指令的输入模式与当前指令参数输入类型不同！ (需要一个dict)", "warning")
                return None
            
            #判断数组有没有越界
            if inst.inst_input_value not in inst_value.keys():
                self.Debug("指令值读取错误, 字典中不存在此命令的key", "warning")
                return None
            
            return inst_value[inst.inst_input_value]
        
        elif inst.inst_input_mode == InstructData.ValueInputMode.Const:
            return inst.inst_input_value
        
    def ItemDoubleClicked(self):

        workbench = Workbench(self)
        workbench.InitUrl()

        dialog = QW.QDialog()
        dialog.setWindowFlags(QC.Qt.Dialog | QC.Qt.WindowMinMaxButtonsHint | QC.Qt.WindowCloseButtonHint)
        grid = QW.QGridLayout(dialog)
        grid.setContentsMargins(0,0,0,0)
        grid.addWidget(workbench)

        dialog.exec_()
        workbench._SlotSave()

'''
    工作台界面代码
    此部分从ui文件生成的。如果界面更新需要编译ui文件后替换到这里
'''
from PyQt5 import QtCore, QtWidgets, QtGui
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(1604, 1107)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
        self.gridLayout.setObjectName("gridLayout")
        self.tb_update = QtWidgets.QToolButton(self.centralwidget)
        self.tb_update.setIconSize(QtCore.QSize(20, 20))
        self.tb_update.setAutoRaise(False)
        self.tb_update.setObjectName("tb_update")
        self.gridLayout.addWidget(self.tb_update, 0, 3, 1, 1)
        self.le_url = QtWidgets.QLineEdit(self.centralwidget)
        self.le_url.setObjectName("le_url")
        self.gridLayout.addWidget(self.le_url, 0, 2, 1, 1)
        self.tb_back = QtWidgets.QToolButton(self.centralwidget)
        self.tb_back.setObjectName("tb_back")
        self.gridLayout.addWidget(self.tb_back, 0, 0, 1, 1)
        self.widget = QtWidgets.QWidget(self.centralwidget)
        self.widget.setStyleSheet("background-color: rgb(231, 229, 255);")
        self.widget.setObjectName("widget")
        self.gridLayout.addWidget(self.widget, 1, 0, 1, 4)
        self.tb_advance = QtWidgets.QToolButton(self.centralwidget)
        self.tb_advance.setObjectName("tb_advance")
        self.gridLayout.addWidget(self.tb_advance, 0, 1, 1, 1)
        MainWindow.setCentralWidget(self.centralwidget)
        self.toolBar = QtWidgets.QToolBar(MainWindow)
        self.toolBar.setAllowedAreas(QtCore.Qt.LeftToolBarArea)
        self.toolBar.setOrientation(QtCore.Qt.Vertical)
        self.toolBar.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
        self.toolBar.setFloatable(True)
        self.toolBar.setObjectName("toolBar")
        MainWindow.addToolBar(QtCore.Qt.LeftToolBarArea, self.toolBar)
        self.dw_flow = QtWidgets.QDockWidget(MainWindow)
        self.dw_flow.setMinimumSize(QtCore.QSize(144, 111))
        self.dw_flow.setObjectName("dw_flow")
        self.dockWidgetContents = QtWidgets.QWidget()
        self.dockWidgetContents.setObjectName("dockWidgetContents")
        self.gridLayout_2 = QtWidgets.QGridLayout(self.dockWidgetContents)
        self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
        self.gridLayout_2.setObjectName("gridLayout_2")
        self.tw_flow = QtWidgets.QTreeWidget(self.dockWidgetContents)
        self.tw_flow.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.tw_flow.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
        self.tw_flow.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustIgnored)
        self.tw_flow.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
        self.tw_flow.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
        self.tw_flow.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
        self.tw_flow.setObjectName("tw_flow")
        self.tw_flow.header().setSortIndicatorShown(False)
        self.gridLayout_2.addWidget(self.tw_flow, 0, 0, 4, 1)
        self.tb_up = QtWidgets.QToolButton(self.dockWidgetContents)
        self.tb_up.setObjectName("tb_up")
        self.gridLayout_2.addWidget(self.tb_up, 1, 1, 1, 1)
        self.tb_remove = QtWidgets.QToolButton(self.dockWidgetContents)
        self.tb_remove.setObjectName("tb_remove")
        self.gridLayout_2.addWidget(self.tb_remove, 0, 1, 1, 1)
        self.tb_down = QtWidgets.QToolButton(self.dockWidgetContents)
        self.tb_down.setObjectName("tb_down")
        self.gridLayout_2.addWidget(self.tb_down, 2, 1, 1, 1)
        self.dw_flow.setWidget(self.dockWidgetContents)
        MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(8), self.dw_flow)
        self.dockWidget = QtWidgets.QDockWidget(MainWindow)
        self.dockWidget.setObjectName("dockWidget")
        self.dockWidgetContents_2 = QtWidgets.QWidget()
        self.dockWidgetContents_2.setObjectName("dockWidgetContents_2")
        self.gridLayout_5 = QtWidgets.QGridLayout(self.dockWidgetContents_2)
        self.gridLayout_5.setObjectName("gridLayout_5")
        spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.gridLayout_5.addItem(spacerItem, 3, 2, 1, 1)
        self.groupBox_2 = QtWidgets.QGroupBox(self.dockWidgetContents_2)
        self.groupBox_2.setObjectName("groupBox_2")
        self.gridLayout_4 = QtWidgets.QGridLayout(self.groupBox_2)
        self.gridLayout_4.setObjectName("gridLayout_4")
        self.rb_list = QtWidgets.QRadioButton(self.groupBox_2)
        self.rb_list.setCheckable(True)
        self.rb_list.setChecked(True)
        self.rb_list.setObjectName("rb_list")
        self.gridLayout_4.addWidget(self.rb_list, 1, 0, 1, 2)
        self.rb_dict = QtWidgets.QRadioButton(self.groupBox_2)
        self.rb_dict.setObjectName("rb_dict")
        self.gridLayout_4.addWidget(self.rb_dict, 2, 0, 1, 2)
        self.label_8 = QtWidgets.QLabel(self.groupBox_2)
        self.label_8.setObjectName("label_8")
        self.gridLayout_4.addWidget(self.label_8, 0, 0, 1, 1)
        self.sb_list_input = QtWidgets.QSpinBox(self.groupBox_2)
        self.sb_list_input.setEnabled(True)
        self.sb_list_input.setToolTip("")
        self.sb_list_input.setAccessibleName("")
        self.sb_list_input.setAccessibleDescription("")
        self.sb_list_input.setSpecialValueText("")
        self.sb_list_input.setProperty("showGroupSeparator", False)
        self.sb_list_input.setSuffix("")
        self.sb_list_input.setObjectName("sb_list_input")
        self.gridLayout_4.addWidget(self.sb_list_input, 1, 2, 1, 1)
        self.cb_inst = QtWidgets.QComboBox(self.groupBox_2)
        self.cb_inst.setObjectName("cb_inst")
        self.gridLayout_4.addWidget(self.cb_inst, 0, 1, 1, 2)
        self.rb_const = QtWidgets.QRadioButton(self.groupBox_2)
        self.rb_const.setObjectName("rb_const")
        self.gridLayout_4.addWidget(self.rb_const, 4, 0, 1, 1)
        self.le_dict_input = QtWidgets.QLineEdit(self.groupBox_2)
        self.le_dict_input.setEnabled(False)
        self.le_dict_input.setLayoutDirection(QtCore.Qt.RightToLeft)
        self.le_dict_input.setClearButtonEnabled(True)
        self.le_dict_input.setObjectName("le_dict_input")
        self.gridLayout_4.addWidget(self.le_dict_input, 2, 2, 1, 1)
        self.le_const_input = QtWidgets.QPlainTextEdit(self.groupBox_2)
        self.le_const_input.setEnabled(False)
        self.le_const_input.setMaximumSize(QtCore.QSize(16777215, 120))
        self.le_const_input.setObjectName("le_const_input")
        self.gridLayout_4.addWidget(self.le_const_input, 5, 0, 1, 3)
        self.gridLayout_5.addWidget(self.groupBox_2, 1, 0, 1, 4)
        spacerItem1 = QtWidgets.QSpacerItem(224, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.gridLayout_5.addItem(spacerItem1, 2, 0, 1, 1)
        self.pb_add_flow = QtWidgets.QPushButton(self.dockWidgetContents_2)
        self.pb_add_flow.setObjectName("pb_add_flow")
        self.gridLayout_5.addWidget(self.pb_add_flow, 2, 2, 1, 1)
        self.groupBox = QtWidgets.QGroupBox(self.dockWidgetContents_2)
        self.groupBox.setObjectName("groupBox")
        self.gridLayout_3 = QtWidgets.QGridLayout(self.groupBox)
        self.gridLayout_3.setObjectName("gridLayout_3")
        self.label = QtWidgets.QLabel(self.groupBox)
        self.label.setObjectName("label")
        self.gridLayout_3.addWidget(self.label, 2, 0, 1, 1)
        self.label_6 = QtWidgets.QLabel(self.groupBox)
        self.label_6.setObjectName("label_6")
        self.gridLayout_3.addWidget(self.label_6, 9, 0, 1, 1)
        self.le_class = QtWidgets.QLineEdit(self.groupBox)
        self.le_class.setEnabled(False)
        self.le_class.setClearButtonEnabled(True)
        self.le_class.setObjectName("le_class")
        self.gridLayout_3.addWidget(self.le_class, 7, 2, 1, 1)
        self.le_type = QtWidgets.QLineEdit(self.groupBox)
        self.le_type.setEnabled(False)
        self.le_type.setClearButtonEnabled(True)
        self.le_type.setObjectName("le_type")
        self.gridLayout_3.addWidget(self.le_type, 8, 2, 1, 1)
        self.le_tag_name = QtWidgets.QLineEdit(self.groupBox)
        self.le_tag_name.setEnabled(False)
        self.le_tag_name.setClearButtonEnabled(True)
        self.le_tag_name.setObjectName("le_tag_name")
        self.gridLayout_3.addWidget(self.le_tag_name, 6, 2, 1, 1)
        self.label_9 = QtWidgets.QLabel(self.groupBox)
        self.label_9.setObjectName("label_9")
        self.gridLayout_3.addWidget(self.label_9, 0, 0, 1, 1)
        self.le_xpath = QtWidgets.QLineEdit(self.groupBox)
        self.le_xpath.setEnabled(False)
        self.le_xpath.setClearButtonEnabled(True)
        self.le_xpath.setObjectName("le_xpath")
        self.gridLayout_3.addWidget(self.le_xpath, 0, 1, 1, 2)
        self.le_src = QtWidgets.QLineEdit(self.groupBox)
        self.le_src.setEnabled(False)
        self.le_src.setClearButtonEnabled(True)
        self.le_src.setObjectName("le_src")
        self.gridLayout_3.addWidget(self.le_src, 9, 2, 1, 1)
        self.le_id = QtWidgets.QLineEdit(self.groupBox)
        self.le_id.setEnabled(False)
        self.le_id.setClearButtonEnabled(True)
        self.le_id.setObjectName("le_id")
        self.gridLayout_3.addWidget(self.le_id, 2, 1, 1, 2)
        self.label_3 = QtWidgets.QLabel(self.groupBox)
        self.label_3.setObjectName("label_3")
        self.gridLayout_3.addWidget(self.label_3, 6, 0, 1, 2)
        self.label_4 = QtWidgets.QLabel(self.groupBox)
        self.label_4.setObjectName("label_4")
        self.gridLayout_3.addWidget(self.label_4, 7, 0, 1, 1)
        self.le_name = QtWidgets.QLineEdit(self.groupBox)
        self.le_name.setEnabled(False)
        self.le_name.setClearButtonEnabled(True)
        self.le_name.setObjectName("le_name")
        self.gridLayout_3.addWidget(self.le_name, 5, 1, 1, 2)
        self.pb_light = QtWidgets.QPushButton(self.groupBox)
        self.pb_light.setObjectName("pb_light")
        self.gridLayout_3.addWidget(self.pb_light, 11, 0, 1, 3)
        self.label_7 = QtWidgets.QLabel(self.groupBox)
        self.label_7.setObjectName("label_7")
        self.gridLayout_3.addWidget(self.label_7, 10, 0, 1, 1)
        self.label_5 = QtWidgets.QLabel(self.groupBox)
        self.label_5.setObjectName("label_5")
        self.gridLayout_3.addWidget(self.label_5, 8, 0, 1, 1)
        self.label_2 = QtWidgets.QLabel(self.groupBox)
        self.label_2.setObjectName("label_2")
        self.gridLayout_3.addWidget(self.label_2, 5, 0, 1, 1)
        self.le_href = QtWidgets.QLineEdit(self.groupBox)
        self.le_href.setEnabled(False)
        self.le_href.setClearButtonEnabled(True)
        self.le_href.setObjectName("le_href")
        self.gridLayout_3.addWidget(self.le_href, 10, 2, 1, 1)
        self.line = QtWidgets.QFrame(self.groupBox)
        self.line.setFrameShape(QtWidgets.QFrame.HLine)
        self.line.setFrameShadow(QtWidgets.QFrame.Sunken)
        self.line.setObjectName("line")
        self.gridLayout_3.addWidget(self.line, 1, 0, 1, 3)
        self.gridLayout_5.addWidget(self.groupBox, 0, 0, 1, 4)
        self.pb_update_flow = QtWidgets.QPushButton(self.dockWidgetContents_2)
        self.pb_update_flow.setObjectName("pb_update_flow")
        self.gridLayout_5.addWidget(self.pb_update_flow, 2, 1, 1, 1)
        self.dockWidget.setWidget(self.dockWidgetContents_2)
        MainWindow.addDockWidget(QtCore.Qt.DockWidgetArea(2), self.dockWidget)
        self.action_export = QtWidgets.QAction(MainWindow)
        self.action_export.setObjectName("action_export")
        self.at_load = QtWidgets.QAction(MainWindow)
        self.at_load.setObjectName("at_load")
        self.action_id = QtWidgets.QAction(MainWindow)
        self.action_id.setCheckable(True)
        self.action_id.setObjectName("action_id")
        self.action_class = QtWidgets.QAction(MainWindow)
        self.action_class.setCheckable(True)
        self.action_class.setObjectName("action_class")
        self.actionType = QtWidgets.QAction(MainWindow)
        self.actionType.setCheckable(True)
        self.actionType.setObjectName("actionType")
        self.action_src = QtWidgets.QAction(MainWindow)
        self.action_src.setCheckable(True)
        self.action_src.setObjectName("action_src")
        self.action_name = QtWidgets.QAction(MainWindow)
        self.action_name.setCheckable(True)
        self.action_name.setObjectName("action_name")
        self.action_tag_name = QtWidgets.QAction(MainWindow)
        self.action_tag_name.setCheckable(True)
        self.action_tag_name.setObjectName("action_tag_name")
        self.action_setting = QtWidgets.QAction(MainWindow)
        self.action_setting.setObjectName("action_setting")
        self.actionHref = QtWidgets.QAction(MainWindow)
        self.actionHref.setCheckable(True)
        self.actionHref.setObjectName("actionHref")
        self.action_xpath = QtWidgets.QAction(MainWindow)
        self.action_xpath.setCheckable(True)
        self.action_xpath.setObjectName("action_xpath")
        self.action_accept_skip = QtWidgets.QAction(MainWindow)
        self.action_accept_skip.setCheckable(True)
        self.action_accept_skip.setObjectName("action_accept_skip")
        self.action_save = QtWidgets.QAction(MainWindow)
        self.action_save.setObjectName("action_save")
        self.action_cookie = QtWidgets.QAction(MainWindow)
        self.action_cookie.setCheckable(True)
        self.action_cookie.setObjectName("action_cookie")
        self.toolBar.addAction(self.action_xpath)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.action_id)
        self.toolBar.addAction(self.action_name)
        self.toolBar.addAction(self.action_tag_name)
        self.toolBar.addAction(self.action_class)
        self.toolBar.addAction(self.actionType)
        self.toolBar.addAction(self.action_src)
        self.toolBar.addAction(self.actionHref)
        self.toolBar.addSeparator()
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.action_save)
        self.toolBar.addAction(self.action_export)
        self.toolBar.addAction(self.at_load)
        self.toolBar.addSeparator()
        self.toolBar.addAction(self.action_accept_skip)
        self.toolBar.addAction(self.action_cookie)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "selenium工作台"))
        self.tb_update.setText(_translate("MainWindow", "刷"))
        self.tb_back.setText(_translate("MainWindow", "<-"))
        self.tb_advance.setText(_translate("MainWindow", "->"))
        self.toolBar.setWindowTitle(_translate("MainWindow", "toolBar"))
        self.dw_flow.setWindowTitle(_translate("MainWindow", "操作流程"))
        self.tw_flow.headerItem().setText(0, _translate("MainWindow", "序号"))
        self.tw_flow.headerItem().setText(1, _translate("MainWindow", "选择器"))
        self.tw_flow.headerItem().setText(2, _translate("MainWindow", "操作"))
        self.tw_flow.headerItem().setText(3, _translate("MainWindow", "输入变量"))
        self.tb_up.setToolTip(_translate("MainWindow", "上移选中的指令"))
        self.tb_up.setText(_translate("MainWindow", "↑"))
        self.tb_remove.setToolTip(_translate("MainWindow", "移除选中的指令"))
        self.tb_remove.setText(_translate("MainWindow", "—"))
        self.tb_down.setToolTip(_translate("MainWindow", "下移选中的指令"))
        self.tb_down.setText(_translate("MainWindow", "↓"))
        self.dockWidget.setWindowTitle(_translate("MainWindow", "操作编辑器"))
        self.groupBox_2.setTitle(_translate("MainWindow", "操作编辑"))
        self.rb_list.setText(_translate("MainWindow", "列表变量输入(值索引)："))
        self.rb_dict.setText(_translate("MainWindow", "字典变量输入(key)："))
        self.label_8.setText(_translate("MainWindow", "操作指令："))
        self.rb_const.setText(_translate("MainWindow", "常量内容"))
        self.le_dict_input.setPlaceholderText(_translate("MainWindow", "只支持字符串类型key"))
        self.pb_add_flow.setText(_translate("MainWindow", "添加操作"))
        self.groupBox.setTitle(_translate("MainWindow", "当前选择标签所匹配的选择器内容"))
        self.label.setText(_translate("MainWindow", "ID选择器 内容："))
        self.label_6.setText(_translate("MainWindow", "Src选择器 内容："))
        self.label_9.setText(_translate("MainWindow", "XPath 内容："))
        self.label_3.setText(_translate("MainWindow", "标签 内容："))
        self.label_4.setText(_translate("MainWindow", "类选择器 内容："))
        self.pb_light.setToolTip(_translate("MainWindow", "并不是所有的标签都能高亮，请酌情使用此功能。"))
        self.pb_light.setText(_translate("MainWindow", "高亮当前选择器匹配的内容"))
        self.label_7.setText(_translate("MainWindow", "Href选择器 内容："))
        self.label_5.setText(_translate("MainWindow", "Type选择器 内容："))
        self.label_2.setText(_translate("MainWindow", "Name选择器 内容："))
        self.pb_update_flow.setText(_translate("MainWindow", "更新操作"))
        self.action_export.setText(_translate("MainWindow", "导出配置"))
        self.action_export.setShortcut(_translate("MainWindow", "Ctrl+P"))
        self.at_load.setText(_translate("MainWindow", "加载配置"))
        self.at_load.setToolTip(_translate("MainWindow", "加载现有配置"))
        self.at_load.setShortcut(_translate("MainWindow", "Ctrl+O"))
        self.action_id.setText(_translate("MainWindow", "ID"))
        self.action_id.setToolTip(_translate("MainWindow", "ID 选择器"))
        self.action_id.setShortcut(_translate("MainWindow", "Ctrl+1"))
        self.action_class.setText(_translate("MainWindow", "Class"))
        self.action_class.setToolTip(_translate("MainWindow", "Class选择器"))
        self.action_class.setShortcut(_translate("MainWindow", "Ctrl+4"))
        self.actionType.setText(_translate("MainWindow", "Type"))
        self.actionType.setToolTip(_translate("MainWindow", "Type选择器"))
        self.actionType.setShortcut(_translate("MainWindow", "Ctrl+5"))
        self.action_src.setText(_translate("MainWindow", "Src"))
        self.action_src.setToolTip(_translate("MainWindow", "Src选择器"))
        self.action_src.setShortcut(_translate("MainWindow", "Ctrl+6"))
        self.action_name.setText(_translate("MainWindow", "Name"))
        self.action_name.setToolTip(_translate("MainWindow", "Name选择器"))
        self.action_name.setShortcut(_translate("MainWindow", "Ctrl+2"))
        self.action_tag_name.setText(_translate("MainWindow", "TagName"))
        self.action_tag_name.setToolTip(_translate("MainWindow", "标签名选择器"))
        self.action_tag_name.setShortcut(_translate("MainWindow", "Ctrl+3"))
        self.action_setting.setText(_translate("MainWindow", "设置"))
        self.action_setting.setToolTip(_translate("MainWindow", "模块的基本配置"))
        self.actionHref.setText(_translate("MainWindow", "Href"))
        self.actionHref.setToolTip(_translate("MainWindow", "Href 定位器"))
        self.actionHref.setShortcut(_translate("MainWindow", "Ctrl+7"))
        self.action_xpath.setText(_translate("MainWindow", "xpath"))
        self.action_xpath.setShortcut(_translate("MainWindow", "Ctrl+0"))
        self.action_accept_skip.setText(_translate("MainWindow", "超链跳转"))
        self.action_accept_skip.setShortcut(_translate("MainWindow", "Ctrl+L"))
        self.action_save.setText(_translate("MainWindow", "保存配置"))
        self.action_save.setShortcut(_translate("MainWindow", "Ctrl+S"))
        self.action_cookie.setText(_translate("MainWindow", "Cookie记录"))
        self.action_cookie.setToolTip(_translate("MainWindow", "开启后记录接下来所有的cookie"))
        self.action_cookie.setShortcut(_translate("MainWindow", "Ctrl+I"))

'''文件导出时使用的dilog'''
class Ui_ExportDlg(object):
    def setupUi(self, ExportDlg):
        ExportDlg.setObjectName("ExportDlg")
        ExportDlg.resize(363, 101)
        self.gridLayout = QtWidgets.QGridLayout(ExportDlg)
        self.gridLayout.setObjectName("gridLayout")
        self.le_export_dir = QtWidgets.QLineEdit(ExportDlg)
        self.le_export_dir.setObjectName("le_export_dir")
        self.gridLayout.addWidget(self.le_export_dir, 1, 1, 1, 1)
        self.tb_dir = QtWidgets.QToolButton(ExportDlg)
        self.tb_dir.setObjectName("tb_dir")
        self.gridLayout.addWidget(self.tb_dir, 1, 3, 1, 1)
        self.label_2 = QtWidgets.QLabel(ExportDlg)
        self.label_2.setObjectName("label_2")
        self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
        self.le_file_name = QtWidgets.QLineEdit(ExportDlg)
        self.le_file_name.setObjectName("le_file_name")
        self.gridLayout.addWidget(self.le_file_name, 0, 1, 1, 1)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        spacerItem = QtWidgets.QSpacerItem(387, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)
        self.pb_ok = QtWidgets.QPushButton(ExportDlg)
        self.pb_ok.setObjectName("pb_ok")
        self.horizontalLayout.addWidget(self.pb_ok)
        self.pushButton = QtWidgets.QPushButton(ExportDlg)
        self.pushButton.setObjectName("pushButton")
        self.horizontalLayout.addWidget(self.pushButton)
        self.gridLayout.addLayout(self.horizontalLayout, 3, 0, 1, 4)
        self.label = QtWidgets.QLabel(ExportDlg)
        self.label.setObjectName("label")
        self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
        spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
        self.gridLayout.addItem(spacerItem1, 2, 1, 1, 1)
        self.label_3 = QtWidgets.QLabel(ExportDlg)
        self.label_3.setObjectName("label_3")
        self.gridLayout.addWidget(self.label_3, 0, 3, 1, 1)

        self.retranslateUi(ExportDlg)
        self.pb_ok.clicked.connect(ExportDlg.accept) # type: ignore
        self.pushButton.clicked.connect(ExportDlg.reject) # type: ignore
        QtCore.QMetaObject.connectSlotsByName(ExportDlg)

    def retranslateUi(self, ExportDlg):
        _translate = QtCore.QCoreApplication.translate
        ExportDlg.setWindowTitle(_translate("ExportDlg", "指令集导出"))
        self.tb_dir.setText(_translate("ExportDlg", "..."))
        self.label_2.setText(_translate("ExportDlg", "导出目录："))
        self.pb_ok.setText(_translate("ExportDlg", "确定"))
        self.pushButton.setText(_translate("ExportDlg", "取消"))
        self.label.setText(_translate("ExportDlg", "文件名："))
        self.label_3.setText(_translate("ExportDlg", ".xml"))

'''
    重写的treewidget对象
'''
class FlowTreeWidget(QW.QTreeWidget):

    def __init__(self, parent) -> None:
        super().__init__(parent)
        pass

from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineScript, QWebEngineProfile, QWebEnginePage
from PyQt5.QtWebChannel import QWebChannel

'''重写WebEngineView对象'''
class WenEngineView(QWebEngineView):

    #当通过超链接而打开新窗口是时触发信号
    sig_new_window = QC.pyqtSignal(str)
    
    def __init__(self, parent = None) -> None:
        super().__init__(parent)

        self._workbench = parent

        '''被点下超链接的url'''
        self._url = QC.QUrl() 

        #链接信号，当超链接被点下是获取被点下的url
        self.page().linkHovered.connect(self._SlotLinkHovered)
        
    def createWindow(self, type) -> 'QWebEngineView':
        '''
            重写这个函数，每当超链接需要打开一个新窗口时，都在当前页面处理
        '''
        #判断主界面的允许找链接是否被按下
        is_acc = self._workbench.action_accept_skip.isChecked()

        #如果没被按下，则不处理
        if not is_acc: return 

        self.sig_new_window.emit(self._url.url())
        self.load(self._url)
        return self

    def _SlotLinkHovered(self, url):
        self._url.setUrl(url)

'''
    参数配置工作台
    这个工作台方便捕获网页的元素，直接设置到参数中
'''
class Workbench(QW.QMainWindow, Ui_MainWindow, QC.QObject):

    def __init__(self, script:ScriptBehavior, parent = None) -> None:
        super().__init__(parent)
        self.setupUi(self)

        #脚本对象
        self._script = script

        self.InitParams()

        self.InitWorkbench()

        self.UpdateWebView()

        self.InitJs()

        self.InitConnect()

    def InitParams(self):
        '''
            初始化一些变量
        '''

        #操作指令列表
        self._inst_list = []

        #高亮颜色标签参数头
        self.tag_color_param = '''var color = "cyan";'''
        #取出高亮颜色标签
        self.no_color_param = '''var color = "";'''


        #上次高亮的标签参数头
        self._old_high_light_params = None

        #当前加载的文件路径
        self._cur_file_path = None

    def InitWorkbench(self):
        '''
            初始化界面
        '''
        #创建浏览器视图
        self._web_view = WenEngineView(self)

        #cookie捕获
        self._profile = QWebEngineProfile("storage", self._web_view)
        cookie_store = self._profile.cookieStore()
        cookie_store.cookieAdded.connect(self._SlotCookieAdd)

        webpage = QWebEnginePage(self._profile, self._web_view)
        self._web_view.setPage(webpage)
        
        #cookie保存对象
        self._cookies = []

        #将浏览器视图添加的视图中央
        grid = QW.QGridLayout(self.widget)
        grid.addWidget(self._web_view)
        grid.setContentsMargins(0, 0, 0, 0)

        #默认将类选择器勾选
        self.action_class.setChecked(True)
        self.le_class.setEnabled(True)
        self.pb_light.setEnabled(False)

        #操作流程表头自适应
        self.tw_flow.header().setSectionResizeMode(QW.QHeaderView.Stretch)

        #初始化指令下拉栏
        self.cb_inst.addItems(Instruct.inst_list)

    def InitConnect(self):

        self._web_view.loadFinished.connect(self._SlotLoadFinished)
        self._web_view.urlChanged.connect(self._SlotUrlChanged)
        self.tb_update.clicked.connect(self._SlotUpdateView)

        self.action_id.triggered.connect(self._SlotSelectorChanged)
        self.action_name.triggered.connect(self._SlotSelectorChanged)
        self.action_tag_name.triggered.connect(self._SlotSelectorChanged)
        self.action_class.triggered.connect(self._SlotSelectorChanged)
        self.actionType.triggered.connect(self._SlotSelectorChanged)
        self.action_src.triggered.connect(self._SlotSelectorChanged)
        self.actionHref.triggered.connect(self._SlotSelectorChanged)
        self.action_xpath.triggered.connect(self._SlotSelectorChanged)
        self.action_save.triggered.connect(self._SlotSave)

        self.tb_back.clicked.connect(self._SlotViewBack)
        self.tb_advance.clicked.connect(self._SlotViewAdvances)
        self.cb_inst.currentTextChanged.connect(self._SlotInstComboxChanged)
        self.rb_dict.clicked.connect(self._SlotInputValueTypeRadioChanged)
        self.rb_list.clicked.connect(self._SlotInputValueTypeRadioChanged)
        self.rb_const.clicked.connect(self._SlotInputValueTypeRadioChanged)
        self.pb_add_flow.clicked.connect(self._SlotAddInstClicked)

        self.tb_remove.clicked.connect(self._SlotRemoveInst)
        self.tb_up.clicked.connect(self._SlotInstUp)
        self.tb_down.clicked.connect(self._SlotInstDown)

        self.tw_flow.itemClicked.connect(self._SlotTreeItemClicked)
        self.pb_light.clicked.connect(self._SlotHighLight)
        self.pb_update_flow.clicked.connect(self._SlotUpdateFlow)

        self.le_id.textChanged.connect(self._SlotSelectorTextChagned)
        self.le_name.textChanged.connect(self._SlotSelectorTextChagned)
        self.le_tag_name.textChanged.connect(self._SlotSelectorTextChagned)
        self.le_class.textChanged.connect(self._SlotSelectorTextChagned)
        self.le_xpath.textChanged.connect(self._SlotSelectorTextChagned)

        self.action_export.triggered.connect(self._SlotExport)
        self.at_load.triggered.connect(self._SlotLoad)

    def InitJs(self):
        '''
            js注入初始化
        '''

        #注入webchannel_js脚本,在创建时就注入
        js_code = QWebEngineScript()
        js_code.setSourceCode(qwebchannel_js)
        js_code.setName("qwebchannel_js")
        js_code.setWorldId(QWebEngineScript.MainWorld)
        js_code.setInjectionPoint(QWebEngineScript.DocumentCreation)
        self._web_view.page().profile().scripts().insert(js_code)

        #注入python对象脚本
        py_to_js_obj = QWebEngineScript()
        py_to_js_obj.setSourceCode(create_py_obj)
        py_to_js_obj.setName("py_to_js_obj")
        py_to_js_obj.setWorldId(QWebEngineScript.MainWorld)
        py_to_js_obj.setInjectionPoint(QWebEngineScript.DocumentReady)
        self._web_view.page().profile().scripts().insert(py_to_js_obj)

        #触发点击函数的注入脚本
        onclick_script = QWebEngineScript()
        onclick_script.setSourceCode(onclick_js)
        onclick_script.setName("onclick_js")
        onclick_script.setWorldId(QWebEngineScript.MainWorld)
        onclick_script.setInjectionPoint(QWebEngineScript.DocumentReady)#在DOM READY的时候注入。也可以在开始的时候注入。
        self._web_view.page().profile().scripts().insert(onclick_script)

        #处理回调
        self.channel = QWebChannel()
        self.channel.registerObject("workbench", self)
        self._web_view.page().setWebChannel(self.channel)

    def InitUrl(self):
        '''
            将脚本的url参数设置给自己
        '''
        #将参数的网址读出来并设置到地址栏
        self.le_url.setText(self._script.p_url.value)
        #更新视图
        self.UpdateWebView()

        #将脚本参数中的指令文件也加载出来
        if self._script.p_inst_path.value and os.path.isfile(self._script.p_inst_path.value):
            self.LoadFile(self._script.p_inst_path.value)

    def GetSerial(self):
        '''
            获取当前的序号
        '''
        return len(self._inst_list)

    def GetCurPanelInstData(self):
        '''
            获取当前面板上的指令数据内容
        '''

        inst_data = InstructData()
        inst_data.serial = self.GetSerial()
        inst_data.id = self.le_id.text()
        inst_data.name = self.le_name.text()
        inst_data.tag_name = self.le_tag_name.text()
        inst_data.class_name = self.le_class.text()
        inst_data.type = self.le_type.text()
        inst_data.src = self.le_src.text()
        inst_data.href = self.le_href.text()
        inst_data.xpath = self.le_xpath.text()

        inst_data.instruct = self.cb_inst.currentText()
        
        #判断是否有输入指令
        if self.sb_list_input.isEnabled():
            inst_data.inst_input_mode = InstructData.ValueInputMode.List
            inst_data.inst_input_value = self.sb_list_input.value()

        elif self.le_dict_input.isEnabled():
            inst_data.inst_input_mode = InstructData.ValueInputMode.Dict
            inst_data.inst_input_value = self.le_dict_input.text()

        elif self.le_const_input.isEnabled():
            inst_data.inst_input_mode = InstructData.ValueInputMode.Const
            inst_data.inst_input_value = self.le_const_input.toPlainText()

        return inst_data

    def GetInstDataBySerial(self, serial):
        '''
            通过执行序号获取执行从列表中
        '''

        for inst in self._inst_list:

            if inst.serial == serial:
                return inst

    def UpdateWebView(self):
        '''
            更新网页
        '''

        #获取网址栏网址
        url_str = self.le_url.text()
        self._web_view.load(QC.QUrl(url_str))
    
    def UpdateInstTree(self):
        '''
            更新指令流程树
        '''
        self.tw_flow.clear()

        for inst_data in self._inst_list:
            item = QW.QTreeWidgetItem([
                str(self.GetSerial()), 
                "-", inst_data.instruct, 
                str(inst_data.inst_input_value)
                ])

            self.tw_flow.addTopLevelItem(item)

    def InstSort(self):
        '''
            将指令重新进行排序，按照序号
        '''

        self._inst_list = sorted(self._inst_list, key= lambda inst: inst.serial)

    def AddInstruct(self, inst_data):
        '''
            添加一条数据
        '''
        self._inst_list.append(inst_data)

        value = str(inst_data.inst_input_value) if str(inst_data.inst_input_value) != None else ""

        item = QW.QTreeWidgetItem([
            str(inst_data.serial), 
            inst_data.xpath, inst_data.instruct,
            value
            ])
        item.setData(0, 999, inst_data)
        
        self.tw_flow.addTopLevelItem(item)
        self.tw_flow.setCurrentItem(item)

    def SetEditSelectorValue(self, ac_obj, obj, value):
        '''
            设置操作编辑器的选择器内容值
        '''

        #如果当前值不为空，则开启对应的action
        if  ac_obj and value != "" and value != "None" and value != None:
            
            ac_obj.setChecked(True)
            obj.setEnabled(True)
            obj.setText(value)

        if obj.isEnabled():
            obj.setText(value)

    def SwopItemInfo(self, item1, item2):
        '''
            交换两个item的信息
        '''
        item1_inst = item1.data(0, 999)
        item2_inst = item2.data(0, 999)

        item1.setText(3, str(item2_inst.inst_input_value))
        item2.setText(3, str(item1_inst.inst_input_value))

        item1.setText(2, str(item2_inst.instruct))
        item2.setText(2, str(item1_inst.instruct))

        #交换序号
        temp_serial = item1_inst.serial
        item1_inst.serial = item2_inst.serial
        item2_inst.serial = temp_serial

        item1.setData(0, 999, item2_inst)
        item2.setData(0, 999, item1_inst)

    def UpdateHighLightEnable(self):
        '''
            更新高亮按钮状态

            高亮按钮并不是所有的状态都可被点击。
            只有当选择器中包含 ID, tagName, className, Name至少其中一个且不为空时，才可被使用
        '''

        is_use = False

        if  self.le_id.isEnabled() and self.le_id.text() != "":
            is_use = True

        if  self.le_name.isEnabled() and self.le_name.text() != "":
            is_use = True
        
        if  self.le_tag_name.isEnabled() and self.le_tag_name.text() != "":
            is_use = True
        
        if  self.le_class.isEnabled() and self.le_class.text() != "":
            is_use = True

        if  self.le_xpath.isEnabled() and self.le_xpath.text() != "":
            is_use = True
        
        self.pb_light.setEnabled(is_use)

    def Clear(self):
        self._inst_list.clear()
        self.tw_flow.clear()

    def LoadFile(self, file_path):
        '''
            加载文件
        '''
        
        self._cur_file_path = file_path

        temp_list, self._cookies = Instruct.LoadFromFile(file_path)

        if not temp_list: 
            QW.QMessageBox.warning(self, "错误", "方案读取错误！")
            return 
        
        self.Clear()

        for inst_data in temp_list:
            self.AddInstruct(inst_data)

    def ExportFile(self, file_path):

        #先将指令集排序在保存
        self.InstSort()

        if not file_path: return
        Instruct.ExportInstList(self._inst_list, file_path, self._cookies)

    def CookieToJson(self, cookie):

        data = {"name": bytearray(cookie.name()).decode(), 
                "domain": cookie.domain(), 
                "value": bytearray(cookie.value()).decode(),
                "path": cookie.path(), 
                "expirationDate": cookie.expirationDate().toString(QC.Qt.ISODate)
                }
        
        return data

    def _SlotCookieAdd(self, cookie):

        if not self.action_cookie.isChecked():
            return 
        
        for c in self._cookies:

            #如果name有重复，则移除旧的
            if c["name"] == bytearray(cookie.name()).decode():
                self._cookies.remove(c)
                break

        self._cookies.append(self.CookieToJson(cookie))

    def _SlotInstUp(self):
        
        cur_item = self.tw_flow.currentItem()
        if not cur_item: return

        #获取当前的行号
        cur_row = self.tw_flow.indexFromItem(cur_item).row()
        
        #如果被移动的是0位，则跳过
        if cur_row == 0: return

        #获取上一位item
        up_item = self.tw_flow.topLevelItem(cur_row - 1)
        self.SwopItemInfo(up_item, cur_item)

        self.tw_flow.clearSelection()
        self.tw_flow.setCurrentItem(up_item)

    def _SlotInstDown(self):
        cur_item = self.tw_flow.currentItem()
        if not cur_item: return

        #获取当前的行号
        cur_row = self.tw_flow.indexFromItem(cur_item).row()

        #如果被移动的是最后位，则跳过
        if cur_row == self.tw_flow.topLevelItemCount() - 1: return

        #获取下一位item
        down_item = self.tw_flow.topLevelItem(cur_row + 1)
        self.SwopItemInfo(down_item, cur_item)

        self.tw_flow.clearSelection()
        self.tw_flow.setCurrentItem(down_item)

    def _SlotRemoveInst(self):

        cur_item = self.tw_flow.currentItem()
        if not cur_item: return

        #获取当前的行号
        cur_row = self.tw_flow.indexFromItem(cur_item).row()
        self.tw_flow.takeTopLevelItem(cur_row)
        self._inst_list.remove(cur_item.data(0, 999))

        #移除后，将当前行号及下面所有的指令序号-1
        for index in range(cur_row, self.tw_flow.topLevelItemCount()):

            item = self.tw_flow.topLevelItem(index)
            serial = int(item.text(0)) - 1 
            item.setText(0, str(serial))
            item.data(0, 999).serial = serial

    def _SlotUpdateView(self):
        self.UpdateWebView()
        self._script.p_url.value = self.le_url.text()

    def _SlotViewBack(self):
        self._web_view.back()

    def _SlotViewAdvances(self):
        self._web_view.forward()

    @QC.pyqtSlot(str, str, str, str, str, str, str, result=str)
    def SlotFromJs(self, id, name, tag_name, class_name, type, src, href):
        '''
            每当前端页面点击一个元素后，就会返回这个元素的所有选择器属性。
            我们再根据已勾选选择器进行处筛选处理
        '''
        
        self.SetEditSelectorValue(None, self.le_id, id)
        self.SetEditSelectorValue(None, self.le_name, name)
        self.SetEditSelectorValue(None, self.le_tag_name, tag_name)
        self.SetEditSelectorValue(None, self.le_class, class_name)
        self.SetEditSelectorValue(None, self.le_type, type)
        self.SetEditSelectorValue(None, self.le_src, src)
        self.SetEditSelectorValue(None, self.le_href, href)

        #更新高亮按钮
        self.UpdateHighLightEnable()

    def _SlotLoadFinished(self):
        pass

    def _SlotUrlChanged(self, url:QC.QUrl):

        self.le_url.setText(url.url())

        #如果新的地址与参数的不相同，则改变背景颜色
        if url.url() != self._script.p_url.value:
            self.le_url.setStyleSheet("background-color:'#f7acbc'")
        else:
            self.le_url.setStyleSheet("background-color:white")

    def _SlotSelectorChanged(self):
        '''
            当选择器action变化时，更新操作编辑器内容
        '''

        #如果是xpath被启用，则关闭其他所有
        if self.sender() == self.action_xpath:
            self.le_xpath.setEnabled(self.action_xpath.isChecked())

            #如果一个选择器被取消，要清空编辑器的内容
            self.action_id.setEnabled(not self.action_xpath.isChecked())
            self.action_name.setEnabled(not self.action_xpath.isChecked())
            self.action_tag_name.setEnabled(not self.action_xpath.isChecked())
            self.action_class.setEnabled(not self.action_xpath.isChecked())
            self.actionType.setEnabled(not self.action_xpath.isChecked())
            self.action_src.setEnabled(not self.action_xpath.isChecked())
            self.actionHref.setEnabled(not self.action_xpath.isChecked())

        self.le_id.setEnabled(self.action_id.isChecked() and self.action_id.isEnabled())
        self.le_name.setEnabled(self.action_name.isChecked() and self.action_id.isEnabled())
        self.le_tag_name.setEnabled(self.action_tag_name.isChecked() and self.action_id.isEnabled())
        self.le_class.setEnabled(self.action_class.isChecked() and self.action_id.isEnabled())
        self.le_type.setEnabled(self.actionType.isChecked() and self.action_id.isEnabled())
        self.le_src.setEnabled(self.action_src.isChecked() and self.action_id.isEnabled())
        self.le_href.setEnabled(self.actionHref.isChecked() and self.action_id.isEnabled())

        #如果一个选择器被取消，要清空编辑器的内容
        if not self.action_id.isChecked(): self.le_id.clear()
        if not self.action_name.isChecked(): self.le_name.clear()
        if not self.action_tag_name.isChecked(): self.le_tag_name.clear()
        if not self.action_class.isChecked(): self.le_class.clear()
        if not self.actionType.isChecked(): self.le_type.clear()
        if not self.action_src.isChecked(): self.le_src.clear()
        if not self.actionHref.isChecked(): self.le_href.clear()
        if not self.action_xpath.isChecked(): self.le_xpath.clear()

        self.UpdateHighLightEnable()
    
    def _SlotSave(self):

        self.ExportFile(self._cur_file_path)

    def _SlotInstComboxChanged(self, text):
        '''
            当下拉栏改变时触发
        '''
        if  Instruct.IsHaveValueInstr(text):
            self.rb_list.setEnabled(True)
            self.rb_dict.setEnabled(True)
            self.rb_const.setEnabled(True)


            self._SlotInputValueTypeRadioChanged()
        else:
            self.rb_list.setEnabled(False)
            self.rb_dict.setEnabled(False)
            self.rb_const.setEnabled(False)
            self.sb_list_input.setEnabled(False)
            self.le_dict_input.setEnabled(False)
            self.le_const_input.setEnabled(False)

    def _SlotInputValueTypeRadioChanged(self):
        '''
            输入值类型选择改变时触发
        '''        

        self.sb_list_input.setEnabled(self.rb_list.isChecked())
        self.le_dict_input.setEnabled(self.rb_dict.isChecked())
        self.le_const_input.setEnabled(self.rb_const.isChecked())
    
    def _SlotAddInstClicked(self):
        '''
            添加一条指令
        '''
        inst_data = self.GetCurPanelInstData()
        self.AddInstruct(inst_data)

    def _SlotTreeItemClicked(self, item, column):
        '''
            流程节点被点击触发
        '''

        inst_data = item.data(0, 999)

        self.SetEditSelectorValue(self.action_id, self.le_id, inst_data.id)
        self.SetEditSelectorValue(self.action_name, self.le_name, inst_data.name)
        self.SetEditSelectorValue(self.action_tag_name, self.le_tag_name, inst_data.tag_name)
        self.SetEditSelectorValue(self.action_class, self.le_class, inst_data.class_name)
        self.SetEditSelectorValue(self.actionType, self.le_type, inst_data.type)
        self.SetEditSelectorValue(self.action_src, self.le_src, inst_data.src)
        self.SetEditSelectorValue(self.actionHref, self.le_href, inst_data.href)
        self.SetEditSelectorValue(self.action_xpath, self.le_xpath, inst_data.xpath)

        #更新高亮按钮
        self.UpdateHighLightEnable()

        if inst_data.inst_input_mode == InstructData.ValueInputMode.List:
            self.rb_list.setChecked(True)
            self.sb_list_input.setValue(inst_data.inst_input_value)

        elif inst_data.inst_input_mode == InstructData.ValueInputMode.Dict:
            self.rb_dict.setChecked(True)
            self.le_dict_input.setText(inst_data.inst_input_value)

        elif inst_data.inst_input_mode == InstructData.ValueInputMode.Const:
            self.rb_const.setChecked(True)
            self.le_const_input.setPlainText(inst_data.inst_input_value)
        self.cb_inst.setCurrentText(inst_data.instruct)

    def _SlotSelectorTextChagned(self, text):
        '''
            当选择编辑器内容值变化时出发，主要是为了检测是否可以高亮
        '''
        self.UpdateHighLightEnable()

    def _SlotHighLight(self):

        #首先要移除上次被高亮的标签
        if self._old_high_light_params:
            old_full_str = self.no_color_param + self._old_high_light_params + high_light
            self._web_view.page().runJavaScript(old_full_str)

        #组合高亮参数信息
        cur_params = high_light_params.format(
                self.le_id.text(),
                self.le_name.text(),
                self.le_tag_name.text(),
                self.le_class.text(),
                self.le_type.text(),
                self.le_src.text(),
                self.le_href.text(),
                self.le_xpath.text(),
            )
        
        full_str = self.tag_color_param + cur_params + high_light
        self._web_view.page().runJavaScript(full_str)
        self._old_high_light_params = cur_params

    def _SlotUpdateFlow(self):
        '''
            更新操作流程树当前被点击的执行
        '''
        
        #获取当前操作树当前的item
        item = self.tw_flow.currentItem()
        if not item: return 

        inst_data = self.GetCurPanelInstData()
        inst_data.serial = int(item.text(0))
        item.setData(0, 999, inst_data)

        item.setText(1, str(inst_data.xpath))
        item.setText(2, inst_data.instruct)
        item.setText(3, str(inst_data.inst_input_value))

        #获取旧的指令，替换他
        old_inst = self.GetInstDataBySerial(inst_data.serial)

        old_index = self._inst_list.index(old_inst)
        self._inst_list[old_index] = inst_data

    def _SlotExport(self):
        
        dlg = ExportDlg(self)
        
        result = dlg.exec_()
        if result == QW.QDialog.Accepted:

            file_path = dlg.GetPath()
            self.ExportFile(file_path)

    def _SlotLoad(self):
        
        path = QW.QFileDialog.getOpenFileName(self, "获取文件")

        if not os.path.exists(path[0]): return

        self.LoadFile(path[0])

class ExportDlg(QW.QDialog, Ui_ExportDlg):

    def __init__(self, parent) -> None:
        super().__init__(parent)
        self.setupUi(self)

        self.tb_dir.clicked.connect(self._SlotOpenDir)

        self.le_export_dir.setText(os.path.expanduser("~"))

    def GetPath(self):

        dir = self.le_export_dir.text().replace("\\", "/").rstrip("/")
        return dir + "/" + self.le_file_name.text() + ".xml"

    def _SlotOpenDir(self):

        fileDialog = QW.QFileDialog(self)

        fileDialog.setFileMode(QW.QFileDialog.Directory)
        fileDialog.exec_()
        selectDir = fileDialog.selectedFiles()
        if len(selectDir) > 0:
            self.le_export_dir.setText(selectDir[0])

high_light_params = '''
    var ID = "{}";
    var name = "{}";
    var tag_name = "{}";
    var class_name = "{}";
    var type = "{}";
    var src ="{}";
    var herf = "{}";
    var XPath = "{}";
'''

'''高亮选择器匹配的内容'''
high_light = '''
    function CheckEleValid(src_list, dst_eles)
    {
        //检查目标元素的有效性
        // src_list 为原有效列表，dst_eles为通过document查找出来的列表
        // 将dst_eles中的元素与原列表对比，并取交集，返回新的数组
        
        //创建一个临时的元素列表
        let temp_dst_ele_list = [];
        for (let i = 0; i < dst_eles.length; i++)
        {
            temp_dst_ele_list.push(dst_eles[i]);
        }
        
        //求交集
        return src_list.filter(item=>temp_dst_ele_list.indexOf(item)>-1);
    };

    //从列表中移除一个元素
    function Remove(src_list, value)
    {
        for (let i = 0; i < src_list.length; i++)
        {
            if (src_list[i] == value)
            {
                src_list.splice(i, 1);
                break;
            }
        }
    }

    //如果xpath不为空，则直接优先处理xpath
    if (XPath != "")
		{
			var x_path_result = document.evaluate(XPath, document, null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE);
			for (var i = 0; i < x_path_result.snapshotLength; i++) {

				element = x_path_result.snapshotItem(i);
				element.style.backgroundColor = "cyan";
			}
		}
    
    // ID如果不为空，则直接取出指定ID的元素，无需进行其他判断
    if (ID != "")
    {
        document.getElementById(ID).style.backgroundColor = "cyan";
    }
    
    else
    {
        //创建一个数组，用来比对。数组最后的剩余的元素，即为满足所有需求的元素
        var valid_ele_list = [];
        
        //查找name
        if (name != "")
        {
            let name_eles = document.getElementsByName(name);
            
            for (let i = 0; i < name_eles.length; i++)
            {
                valid_ele_list.push(name_eles[i]);
            }
        }
        //tag_name
        if (tag_name != "")
        {
            let tag_name_eles = document.getElementsByTagName(tag_name);
            
            //如果到这里有效列表为空，则添加,不为空则处理交集
            if (valid_ele_list.length == 0)
            {
                for (let i = 0; i < tag_name_eles.length; i++)
                {
                    valid_ele_list.push(tag_name_eles[i]);
                }
            }
            else
            {
                valid_ele_list = CheckEleValid(valid_ele_list, tag_name_eles);	
            }
        }
        //class_name
        if (class_name != "")
        {
            let class_eles = document.getElementsByClassName(class_name);
            
            //如果到这里有效列表为空，则添加,不为空则处理交集
            if (valid_ele_list.length == 0)
            {
                for (let i = 0; i < class_eles.length; i++)
                {
                    valid_ele_list.push(class_eles[i]);
                }
            }
            else
            {
                valid_ele_list = CheckEleValid(valid_ele_list, class_eles);	
            }
        }
        
        //筛选type src href
        if (type != "")
        {
            
            //需要移除的索引列表。不能直接移除，会导致数组遍历出错
            let remove_index_list = [];
            
            
            for (let index = 0; index < valid_ele_list.length; index ++)
            {
                //如果当前元素的src不等于src，则移除他
                if (valid_ele_list[index].attributes.type.value != type)
                {
                    remove_index_list.push(valid_ele_list[index]);
                }
            }
            
            for (let rj = 0; rj < remove_index_list.length; rj ++)
            {
                // console.log(remove_index_list[rj], " -> ", "被移除");
                Remove(valid_ele_list, remove_index_list[rj]);
            }
        }
        
        //筛选type src href
        if (src != "")
        {
            
            //需要移除的索引列表。不能直接移除，会导致数组遍历出错
            let remove_index_list = [];
            
            
            for (let index = 0; index < valid_ele_list.length; index ++)
            {
                //如果当前元素的src不等于src，则移除他
                if (valid_ele_list[index].attributes.src.value != src)
                {
                    remove_index_list.push(valid_ele_list[index]);
                }
            }
            
            for (let rj = 0; rj < remove_index_list.length; rj ++)
            {
                // console.log(remove_index_list[rj], " -> ", "被移除");
                Remove(valid_ele_list, remove_index_list[rj]);
            }
        }
        
        //筛选type src href
        if (herf != "")
        {
            
            //需要移除的索引列表。不能直接移除，会导致数组遍历出错
            let remove_index_list = [];
            
            
            for (let index = 0; index < valid_ele_list.length; index ++)
            {
                //如果当前元素的herf不等于herf，则移除他
                if (valid_ele_list[index].attributes.herf.value != herf)
                {
                    remove_index_list.push(valid_ele_list[index]);
                }
            }
            
            for (let rj = 0; rj < remove_index_list.length; rj ++)
            {
                // console.log(remove_index_list[rj], " -> ", "被移除");
                Remove(valid_ele_list, remove_index_list[rj]);
            }
        }
        
        for (let index = 0; index < valid_ele_list.length; index ++)
        {
            valid_ele_list[index].style.backgroundColor = color;
        }
    }
'''

'''在js中创建python对象的代码'''
create_py_obj = '''
if (typeof qt !== "undefined") {

    new QWebChannel(qt.webChannelTransport, function (channel) {
        window.workbench = channel.objects.workbench;
    });
}
else
{
    alert("qt not defined");
}

'''

'''js点击回调源码'''
onclick_js = '''
document.onclick = function(a)
{
    window.workbench.SlotFromJs(
        a.target.id,
        a.target.name,
        a.target.tagName,
        a.target.className,
        a.target.type,
        a.target.src,
        a.target.href
        );
}
'''

'''qwebchannel.hs 源码'''
qwebchannel_js = '''
/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtWebChannel module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

"use strict";

var QWebChannelMessageTypes = {
    signal: 1,
    propertyUpdate: 2,
    init: 3,
    idle: 4,
    debug: 5,
    invokeMethod: 6,
    connectToSignal: 7,
    disconnectFromSignal: 8,
    setProperty: 9,
    response: 10,
};

var QWebChannel = function(transport, initCallback)
{
    if (typeof transport !== "object" || typeof transport.send !== "function") {
        console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." +
                      " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send));
        return;
    }

    var channel = this;
    this.transport = transport;

    this.send = function(data)
    {
        if (typeof(data) !== "string") {
            data = JSON.stringify(data);
        }
        channel.transport.send(data);
    }

    this.transport.onmessage = function(message)
    {
        var data = message.data;
        if (typeof data === "string") {
            data = JSON.parse(data);
        }
        switch (data.type) {
            case QWebChannelMessageTypes.signal:
                channel.handleSignal(data);
                break;
            case QWebChannelMessageTypes.response:
                channel.handleResponse(data);
                break;
            case QWebChannelMessageTypes.propertyUpdate:
                channel.handlePropertyUpdate(data);
                break;
            default:
                console.error("invalid message received:", message.data);
                break;
        }
    }

    this.execCallbacks = {};
    this.execId = 0;
    this.exec = function(data, callback)
    {
        if (!callback) {
            // if no callback is given, send directly
            channel.send(data);
            return;
        }
        if (channel.execId === Number.MAX_VALUE) {
            // wrap
            channel.execId = Number.MIN_VALUE;
        }
        if (data.hasOwnProperty("id")) {
            console.error("Cannot exec message with property id: " + JSON.stringify(data));
            return;
        }
        data.id = channel.execId++;
        channel.execCallbacks[data.id] = callback;
        channel.send(data);
    };

    this.objects = {};

    this.handleSignal = function(message)
    {
        var object = channel.objects[message.object];
        if (object) {
            object.signalEmitted(message.signal, message.args);
        } else {
            console.warn("Unhandled signal: " + message.object + "::" + message.signal);
        }
    }

    this.handleResponse = function(message)
    {
        if (!message.hasOwnProperty("id")) {
            console.error("Invalid response message received: ", JSON.stringify(message));
            return;
        }
        channel.execCallbacks[message.id](message.data);
        delete channel.execCallbacks[message.id];
    }

    this.handlePropertyUpdate = function(message)
    {
        for (var i in message.data) {
            var data = message.data[i];
            var object = channel.objects[data.object];
            if (object) {
                object.propertyUpdate(data.signals, data.properties);
            } else {
                console.warn("Unhandled property update: " + data.object + "::" + data.signal);
            }
        }
        channel.exec({type: QWebChannelMessageTypes.idle});
    }

    this.debug = function(message)
    {
        channel.send({type: QWebChannelMessageTypes.debug, data: message});
    };

    channel.exec({type: QWebChannelMessageTypes.init}, function(data) {
        for (var objectName in data) {
            var object = new QObject(objectName, data[objectName], channel);
        }
        // now unwrap properties, which might reference other registered objects
        for (var objectName in channel.objects) {
            channel.objects[objectName].unwrapProperties();
        }
        if (initCallback) {
            initCallback(channel);
        }
        channel.exec({type: QWebChannelMessageTypes.idle});
    });
};

function QObject(name, data, webChannel)
{
    this.__id__ = name;
    webChannel.objects[name] = this;

    // List of callbacks that get invoked upon signal emission
    this.__objectSignals__ = {};

    // Cache of all properties, updated when a notify signal is emitted
    this.__propertyCache__ = {};

    var object = this;

    // ----------------------------------------------------------------------

    this.unwrapQObject = function(response)
    {
        if (response instanceof Array) {
            // support list of objects
            var ret = new Array(response.length);
            for (var i = 0; i < response.length; ++i) {
                ret[i] = object.unwrapQObject(response[i]);
            }
            return ret;
        }
        if (!response
            || !response["__QObject*__"]
            || response.id === undefined) {
            return response;
        }

        var objectId = response.id;
        if (webChannel.objects[objectId])
            return webChannel.objects[objectId];

        if (!response.data) {
            console.error("Cannot unwrap unknown QObject " + objectId + " without data.");
            return;
        }

        var qObject = new QObject( objectId, response.data, webChannel );
        qObject.destroyed.connect(function() {
            if (webChannel.objects[objectId] === qObject) {
                delete webChannel.objects[objectId];
                // reset the now deleted QObject to an empty {} object
                // just assigning {} though would not have the desired effect, but the
                // below also ensures all external references will see the empty map
                // NOTE: this detour is necessary to workaround QTBUG-40021
                var propertyNames = [];
                for (var propertyName in qObject) {
                    propertyNames.push(propertyName);
                }
                for (var idx in propertyNames) {
                    delete qObject[propertyNames[idx]];
                }
            }
        });
        // here we are already initialized, and thus must directly unwrap the properties
        qObject.unwrapProperties();
        return qObject;
    }

    this.unwrapProperties = function()
    {
        for (var propertyIdx in object.__propertyCache__) {
            object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
        }
    }

    function addSignal(signalData, isPropertyNotifySignal)
    {
        var signalName = signalData[0];
        var signalIndex = signalData[1];
        object[signalName] = {
            connect: function(callback) {
                if (typeof(callback) !== "function") {
                    console.error("Bad callback given to connect to signal " + signalName);
                    return;
                }

                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
                object.__objectSignals__[signalIndex].push(callback);

                if (!isPropertyNotifySignal && signalName !== "destroyed") {
                    // only required for "pure" signals, handled separately for properties in propertyUpdate
                    // also note that we always get notified about the destroyed signal
                    webChannel.exec({
                        type: QWebChannelMessageTypes.connectToSignal,
                        object: object.__id__,
                        signal: signalIndex
                    });
                }
            },
            disconnect: function(callback) {
                if (typeof(callback) !== "function") {
                    console.error("Bad callback given to disconnect from signal " + signalName);
                    return;
                }
                object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
                var idx = object.__objectSignals__[signalIndex].indexOf(callback);
                if (idx === -1) {
                    console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
                    return;
                }
                object.__objectSignals__[signalIndex].splice(idx, 1);
                if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
                    // only required for "pure" signals, handled separately for properties in propertyUpdate
                    webChannel.exec({
                        type: QWebChannelMessageTypes.disconnectFromSignal,
                        object: object.__id__,
                        signal: signalIndex
                    });
                }
            }
        };
    }

    /**
     * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
     */
    function invokeSignalCallbacks(signalName, signalArgs)
    {
        var connections = object.__objectSignals__[signalName];
        if (connections) {
            connections.forEach(function(callback) {
                callback.apply(callback, signalArgs);
            });
        }
    }

    this.propertyUpdate = function(signals, propertyMap)
    {
        // update property cache
        for (var propertyIndex in propertyMap) {
            var propertyValue = propertyMap[propertyIndex];
            object.__propertyCache__[propertyIndex] = propertyValue;
        }

        for (var signalName in signals) {
            // Invoke all callbacks, as signalEmitted() does not. This ensures the
            // property cache is updated before the callbacks are invoked.
            invokeSignalCallbacks(signalName, signals[signalName]);
        }
    }

    this.signalEmitted = function(signalName, signalArgs)
    {
        invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
    }

    function addMethod(methodData)
    {
        var methodName = methodData[0];
        var methodIdx = methodData[1];
        object[methodName] = function() {
            var args = [];
            var callback;
            for (var i = 0; i < arguments.length; ++i) {
                var argument = arguments[i];
                if (typeof argument === "function")
                    callback = argument;
                else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
                    args.push({
                        "id": argument.__id__
                    });
                else
                    args.push(argument);
            }

            webChannel.exec({
                "type": QWebChannelMessageTypes.invokeMethod,
                "object": object.__id__,
                "method": methodIdx,
                "args": args
            }, function(response) {
                if (response !== undefined) {
                    var result = object.unwrapQObject(response);
                    if (callback) {
                        (callback)(result);
                    }
                }
            });
        };
    }

    function bindGetterSetter(propertyInfo)
    {
        var propertyIndex = propertyInfo[0];
        var propertyName = propertyInfo[1];
        var notifySignalData = propertyInfo[2];
        // initialize property cache with current value
        // NOTE: if this is an object, it is not directly unwrapped as it might
        // reference other QObject that we do not know yet
        object.__propertyCache__[propertyIndex] = propertyInfo[3];

        if (notifySignalData) {
            if (notifySignalData[0] === 1) {
                // signal name is optimized away, reconstruct the actual name
                notifySignalData[0] = propertyName + "Changed";
            }
            addSignal(notifySignalData, true);
        }

        Object.defineProperty(object, propertyName, {
            configurable: true,
            get: function () {
                var propertyValue = object.__propertyCache__[propertyIndex];
                if (propertyValue === undefined) {
                    // This shouldn't happen
                    console.warn("Undefined value in property cache for property " + object.__id__);
                }

                return propertyValue;
            },
            set: function(value) {
                if (value === undefined) {
                    console.warn("Property setter for " + propertyName + " called with undefined value!");
                    return;
                }
                object.__propertyCache__[propertyIndex] = value;
                var valueToSend = value;
                if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
                    valueToSend = { "id": valueToSend.__id__ };
                webChannel.exec({
                    "type": QWebChannelMessageTypes.setProperty,
                    "object": object.__id__,
                    "property": propertyIndex,
                    "value": valueToSend
                });
            }
        });

    }

    // ----------------------------------------------------------------------

    data.methods.forEach(addMethod);

    data.properties.forEach(bindGetterSetter);

    data.signals.forEach(function(signal) { addSignal(signal, false); });

    for (var name in data.enums) {
        object[name] = data.enums[name];
    }
}

//required for use with nodejs
if (typeof module === 'object') {
    module.exports = {
        QWebChannel: QWebChannel
    };
}

'''
